Passed
Branch v10.2.x (9ba6c0)
by Rafael S.
02:38
created

WaveFileConverter   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 37
eloc 151
dl 0
loc 306
rs 9.44
c 0
b 0
f 0

16 Functions

Rating   Name   Duplication   Size   Complexity  
A fromALaw 0 11 2
A outputSize_ 0 8 2
A fromIMAADPCM 0 11 2
A assure16Bit_ 0 6 2
A toBitDepth 0 31 4
B toSampleRate 0 31 6
A toIMAADPCM 0 20 3
A toRIFF 0 7 1
A correctContainer_ 0 3 2
A toRIFX 0 8 1
A assureUncompressed_ 0 9 4
A toALaw 0 12 1
A toMuLaw 0 12 1
A validateResample_ 0 9 3
A fromExisting_ 0 8 1
A fromMuLaw 0 11 2
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileConverter class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { changeBitDepth as bitDepthLib } from 'bitdepth';
31
import * as imaadpcm from 'imaadpcm';
32
import * as alawmulaw from 'alawmulaw';
33
import { unpackArray, unpackArrayTo } from 'byte-data';
34
import { WaveFileMetaEditor } from './wavefile-meta-editor';
35
import { truncateSamples, truncateIntSamples } from './truncate-samples';
36
import validateSampleRate from './validate-sample-rate';
37
import { resample } from './resampler';
38
39
/**
40
 * A class to convert wav files to other types of wav files.
41
 * @extends WaveFileMetaEditor
42
 * @ignore
43
 */
44
export class WaveFileConverter extends WaveFileMetaEditor {
45
46
  /**
47
   * Force a file as RIFF.
48
   */
49
  toRIFF() {
50
    this.fromExisting_(
51
      this.fmt.numChannels,
52
      this.fmt.sampleRate,
53
      this.bitDepth,
54
      unpackArray(this.data.samples, this.dataType));
55
  }
56
57
  /**
58
   * Force a file as RIFX.
59
   */
60
  toRIFX() {
61
    this.fromExisting_(
62
      this.fmt.numChannels,
63
      this.fmt.sampleRate,
64
      this.bitDepth,
65
      unpackArray(this.data.samples, this.dataType),
66
      {container: 'RIFX'});
67
  }
68
69
  /**
70
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
71
   * @throws {Error} If sample rate is not 8000.
72
   * @throws {Error} If number of channels is not 1.
73
   */
74
  toIMAADPCM() {
75
    if (this.fmt.sampleRate !== 8000) {
76
      throw new Error(
77
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
78
    } else if (this.fmt.numChannels !== 1) {
79
      throw new Error(
80
        'Only mono files can be compressed as IMA-ADPCM.');
81
    } else {
82
      this.assure16Bit_();
83
      /** @type {!Int16Array} */
84
      let output = new Int16Array(this.outputSize_());
85
      unpackArrayTo(this.data.samples, this.dataType, output);
86
      this.fromExisting_(
87
        this.fmt.numChannels,
88
        this.fmt.sampleRate,
89
        '4',
90
        imaadpcm.encode(output),
91
        {container: this.correctContainer_()});
92
    }
93
  }
94
95
  /**
96
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
97
   * @param {string} bitDepthCode The new bit depth of the samples.
98
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
99
   *    Optional. Default is 16.
100
   */
101
  fromIMAADPCM(bitDepthCode='16') {
102
    this.fromExisting_(
103
      this.fmt.numChannels,
104
      this.fmt.sampleRate,
105
      '16',
106
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
107
      {container: this.correctContainer_()});
108
    if (bitDepthCode != '16') {
109
      this.toBitDepth(bitDepthCode);
110
    }
111
  }
112
113
  /**
114
   * Encode a 16-bit wave file as 8-bit A-Law.
115
   */
116
  toALaw() {
117
    this.assure16Bit_();
118
    /** @type {!Int16Array} */
119
    let output = new Int16Array(this.outputSize_());
120
    unpackArrayTo(this.data.samples, this.dataType, output);
121
    this.fromExisting_(
122
      this.fmt.numChannels,
123
      this.fmt.sampleRate,
124
      '8a',
125
      alawmulaw.alaw.encode(output),
126
      {container: this.correctContainer_()});
127
  }
128
129
  /**
130
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
131
   * @param {string} bitDepthCode The new bit depth of the samples.
132
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
133
   *    Optional. Default is 16.
134
   */
135
  fromALaw(bitDepthCode='16') {
136
    this.fromExisting_(
137
      this.fmt.numChannels,
138
      this.fmt.sampleRate,
139
      '16',
140
      alawmulaw.alaw.decode(this.data.samples),
141
      {container: this.correctContainer_()});
142
    if (bitDepthCode != '16') {
143
      this.toBitDepth(bitDepthCode);
144
    }
145
  }
146
147
  /**
148
   * Encode 16-bit wave file as 8-bit mu-Law.
149
   */
150
  toMuLaw() {
151
    this.assure16Bit_();
152
    /** @type {!Int16Array} */
153
    let output = new Int16Array(this.outputSize_());
154
    unpackArrayTo(this.data.samples, this.dataType, output);
155
    this.fromExisting_(
156
      this.fmt.numChannels,
157
      this.fmt.sampleRate,
158
      '8m',
159
      alawmulaw.mulaw.encode(output),
160
      {container: this.correctContainer_()});
161
  }
162
163
  /**
164
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
165
   * @param {string} bitDepthCode The new bit depth of the samples.
166
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
167
   *    Optional. Default is 16.
168
   */
169
  fromMuLaw(bitDepthCode='16') {
170
    this.fromExisting_(
171
      this.fmt.numChannels,
172
      this.fmt.sampleRate,
173
      '16',
174
      alawmulaw.mulaw.decode(this.data.samples),
175
      {container: this.correctContainer_()});
176
    if (bitDepthCode != '16') {
177
      this.toBitDepth(bitDepthCode);
178
    }
179
  }
180
181
  /**
182
   * Change the bit depth of the samples.
183
   * @param {string} newBitDepth The new bit depth of the samples.
184
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
185
   * @param {boolean} changeResolution A boolean indicating if the
186
   *    resolution of samples should be actually changed or not.
187
   * @throws {Error} If the bit depth is not valid.
188
   */
189
  toBitDepth(newBitDepth, changeResolution=true) {
190
    /** @type {string} */
191
    let toBitDepth = newBitDepth;
192
    /** @type {string} */
193
    let thisBitDepth = this.bitDepth;
194
    if (!changeResolution) {
195
      if (newBitDepth != '32f') {
196
        toBitDepth = this.dataType.bits.toString();
197
      }
198
      thisBitDepth = '' + this.dataType.bits;
199
    }
200
    this.assureUncompressed_();
201
    /** @type {number} */
202
    let sampleCount = this.data.samples.length / (this.dataType.bits / 8);
203
    /** @type {!Float64Array} */
204
    let typedSamplesInput = new Float64Array(sampleCount);
205
    /** @type {!Float64Array} */
206
    let typedSamplesOutput = new Float64Array(sampleCount);
207
    unpackArrayTo(this.data.samples, this.dataType, typedSamplesInput);
208
    if (thisBitDepth == "32f" || thisBitDepth == "64") {
209
      truncateSamples(typedSamplesInput);
210
    }
211
    bitDepthLib(
212
      typedSamplesInput, thisBitDepth, toBitDepth, typedSamplesOutput);
213
    this.fromExisting_(
214
      this.fmt.numChannels,
215
      this.fmt.sampleRate,
216
      newBitDepth,
217
      typedSamplesOutput,
218
      {container: this.correctContainer_()});
219
  }
220
221
  /**
222
   * Convert the sample rate of the file.
223
   * @param {number} sampleRate The target sample rate.
224
   * @param {?Object} details The extra configuration, if needed.
225
   */
226
  toSampleRate(sampleRate, details={}) {
227
    this.validateResample_(sampleRate);
228
    /** @type {!Array|!TypedArray} */
229
    let samples = this.getSamples();
230
    /** @type {!Array|!Float64Array} */
231
    let newSamples = [];
232
    // Mono files
233
    if (samples.constructor === Float64Array) {
234
      newSamples = resample(samples, this.fmt.sampleRate, sampleRate, details);
235
    // Multi-channel files
236
    } else {
237
      for (let i = 0; i < samples.length; i++) {
238
        newSamples.push(resample(
239
          samples[i], this.fmt.sampleRate, sampleRate, details));
240
      }
241
    }
242
    // Truncate samples
243
    if (this.bitDepth !== '64' && this.bitDepth !== '32f') {
244
      if (newSamples[0].constructor === Number) {
245
        truncateIntSamples(newSamples, this.dataType.bits);
246
      } else {
247
        for (let i = 0; i < newSamples.length; i++) {
248
          truncateIntSamples(newSamples[i], this.dataType.bits);
249
        }
250
      }
251
    }
252
    // Recreate the file
253
    this.fromExisting_(
254
      this.fmt.numChannels, sampleRate, this.bitDepth, newSamples,
255
      {'container': this.correctContainer_()});
256
  }
257
258
  /**
259
   * Validate the conditions for resampling.
260
   * @param {number} sampleRate The target sample rate.
261
   * @throws {Error} If the file cant be resampled.
262
   * @private
263
   */
264
  validateResample_(sampleRate) {
265
    if (!validateSampleRate(
266
        this.fmt.numChannels, this.fmt.bitsPerSample, sampleRate)) {
267
      throw new Error('Invalid sample rate.');
268
    } else if (['4','8a','8m'].indexOf(this.bitDepth) > -1) {
269
      throw new Error(
270
        'wavefile can\'t change the sample rate of compressed files.');
271
    }
272
  }
273
274
  /**
275
   * Make the file 16-bit if it is not.
276
   * @private
277
   */
278
  assure16Bit_() {
279
    this.assureUncompressed_();
280
    if (this.bitDepth != '16') {
281
      this.toBitDepth('16');
282
    }
283
  }
284
285
  /**
286
   * Uncompress the samples in case of a compressed file.
287
   * @private
288
   */
289
  assureUncompressed_() {
290
    if (this.bitDepth == '8a') {
291
      this.fromALaw();
292
    } else if (this.bitDepth == '8m') {
293
      this.fromMuLaw();
294
    } else if (this.bitDepth == '4') {
295
      this.fromIMAADPCM();
296
    }
297
  }
298
299
  /**
300
   * Return 'RIFF' if the container is 'RF64', the current container name
301
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
302
   * @return {string}
303
   * @private
304
   */
305
  correctContainer_() {
306
    return this.container == 'RF64' ? 'RIFF' : this.container;
307
  }
308
309
  /**
310
   * Set up the WaveFileCreator object based on the arguments passed.
311
   * This method only reset the fmt , fact, ds64 and data chunks.
312
   * @param {number} numChannels The number of channels
313
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
314
   * @param {number} sampleRate The sample rate.
315
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
316
   * @param {string} bitDepthCode The audio bit depth code.
317
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
318
   *    or any value between '8' and '32' (like '12').
319
   * @param {!Array|!TypedArray} samples
320
   *    The samples. Must be in the correct range according to the bit depth.
321
   * @param {?Object} options Optional. Used to force the container
322
   *    as RIFX with {'container': 'RIFX'}
323
   * @throws {Error} If any argument does not meet the criteria.
324
   * @private
325
   */
326
  fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
327
    let tmpWav = new WaveFileMetaEditor();
328
    Object.assign(this.fmt, tmpWav.fmt);
329
    Object.assign(this.fact, tmpWav.fact);
330
    Object.assign(this.ds64, tmpWav.ds64);
331
    Object.assign(this.data, tmpWav.data);
332
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
333
  }
334
335
  /**
336
   * Return the size in bytes of the output sample array when applying
337
   * compression to 16-bit samples.
338
   * @return {number}
339
   * @private
340
   */
341
  outputSize_() {
342
    /** @type {number} */
343
    let outputSize = this.data.samples.length / 2;
344
    if (outputSize % 2) {
345
      outputSize++;
346
    }
347
    return outputSize;
348
  }
349
}
350